這篇教學會延伸「Mediapipe 手掌辨識」和「在影片中即時繪圖」文章,並應用「OpenCV 影像遮罩」功能,實作一個「用手指擦除鏡子霧氣」的趣味效果 ( 食指和中指分開時不會擦除,食指中指併攏就會擦除 )。
因為程式使用 Jupyter 搭配 Tensorflow 進行開發,所以請先閱讀「使用 Anaconda」和「使用 MediaPipe」,安裝對應的套件,如果不要使用 Juputer,也可參考「使用 Python 虛擬環境」,建立虛擬環境進行實作。

參考「影像遮罩」文章,實作「清楚影像裡面,有一個模糊區域」的效果,過程的原理如下:
- 使用 NumPy 產生兩個遮罩,一個遮罩給「清楚的影像」,一個遮罩給「模糊的影像」。
 - 清楚影像的遮罩,需要套用模糊的部分為黑色,其他為白色。
 - 模糊影像的遮罩,需要套用模糊的部分為白色,其他為黑色。
 - 將遮罩轉換為灰階後,使用 cv2.bitwise_and 方法套用遮罩。
 - 遮罩套用完成,使用 cv2.add 方法合併影像。
 

下方的程式碼執行後,會在攝影機的影像中,即時套用遮罩的效果。
import cv2
import numpy as np
w = 640    # 定義影片寬度
h = 360    # 定義影像高度
dots = []  # 記錄座標
mask_b = np.zeros((h,w,3), dtype='uint8')   # 產生黑色遮罩 -> 套用清楚影像
mask_b[:, :] = 255                          # 設定黑色遮罩底色為白色
mask_b[80:280, 220:420] = 0                 # 設定黑色遮罩哪個區域是黑色
mask_w = np.zeros((h,w,3), dtype='uint8')   # 產生白色遮罩 -> 套用模糊影像
mask_w[80:280, 220:420] = 255               # 設定白色遮罩哪個區域是白色
cap = cv2.VideoCapture(0)
if not cap.isOpened():
    print("Cannot open camera")
    exit()
while True:
    ret, img = cap.read()
    if not ret:
        print("Cannot receive frame")
        break
    img = cv2.resize(img,(w,h))                      # 縮小尺寸,加快速度
    img = cv2.flip(img, 1)                           # 翻轉影像
    img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)      # 轉換顏色為 BGRA ( 計算時需要用到 Alpha 色版 )
    img2 = img.copy()                                # 複製影像
    img2 = cv2.blur(img, (55, 55))                   # 套用模糊
    mask1 = cv2.cvtColor(mask_b, cv2.COLOR_BGR2GRAY) # 轉換遮罩為灰階
    img = cv2.bitwise_and(img, img, mask=mask1)      # 清楚影像套用黑遮罩
    mask2 = cv2.cvtColor(mask_w, cv2.COLOR_BGR2GRAY) # 轉換遮罩為灰階
    img2 = cv2.bitwise_and(img2, img2, mask=mask2)   # 模糊影像套用白遮罩
    output = cv2.add(img, img2)                      # 合併影像
    cv2.imshow('oxxostudio', output)
    if cv2.waitKey(50) == ord('q'):
        break
cap.release()
cv2.destroyAllWindows()

了解模糊和清楚合併的原理後,參考「在影片中即時繪圖」文章,將滑鼠繪圖的程式加入到程式裡,執行後就可以在模糊的影像中,用滑鼠擦出清楚的影像。
import cv2
import numpy as np
w = 640    # 定義影片寬度
h = 360    # 定義影像高度
dots = []  # 記錄座標
mask_b = np.zeros((h,w,3), dtype='uint8')   # 產生黑色遮罩 -> 套用清楚影像
mask_w = np.zeros((h,w,3), dtype='uint8')   # 產生白色遮罩 -> 套用模糊影像
mask_w[0:h, 0:w] = 255                      # 白色遮罩背景為白色
# 滑鼠繪圖函式
def show_xy(event,x,y,flags,param):
    global dots, mask
    if flags == 1:
        if event == 1:
            dots.append([x,y])
        if event == 4:
            dots = []
        if event == 0 or event == 4:
            dots.append([x,y])
            x1 = dots[len(dots)-2][0]
            y1 = dots[len(dots)-2][1]
            x2 = dots[len(dots)-1][0]
            y2 = dots[len(dots)-1][1]
            cv2.line(mask_w, (x1,y1), (x2,y2), (0,0,0), 50)        # 在白色遮罩上畫出黑色線條
            cv2.line(mask_b, (x1,y1), (x2,y2), (255,255,255), 50)  # 在黑色遮罩上畫出白色線條
cv2.imshow('oxxostudio', mask)                 # 啟用視窗
cv2.setMouseCallback('oxxostudio', show_xy)    # 偵測滑鼠行為
cap = cv2.VideoCapture(0)
if not cap.isOpened():
    print("Cannot open camera")
    exit()
while True:
    ret, img = cap.read()
    if not ret:
        print("Cannot receive frame")
        break
    img = cv2.resize(img,(w,h))                      # 縮小尺寸,加快速度
    img = cv2.flip(img, 1)                           # 翻轉影像
    img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)      # 轉換顏色為 BGRA ( 計算時需要用到 Alpha 色版 )
    img2 = img.copy()                                # 複製影像
    img2 = cv2.blur(img, (55, 55))                   # 套用模糊
    mask1 = cv2.cvtColor(mask_b, cv2.COLOR_BGR2GRAY) # 轉換遮罩為灰階
    img = cv2.bitwise_and(img, img, mask=mask1)      # 清楚影像套用黑遮罩
    mask2 = cv2.cvtColor(mask_w, cv2.COLOR_BGR2GRAY) # 轉換遮罩為灰階
    img2 = cv2.bitwise_and(img2, img2, mask=mask2)   # 模糊影像套用白遮罩
    output = cv2.add(img, img2)                      # 合併影像
    cv2.imshow('oxxostudio', output)
    if cv2.waitKey(50) == ord('q'):
        break
cap.release()
cv2.destroyAllWindows()

參考「Mediapipe 手掌偵測」文章,偵測手指的座標,就能透過手指擦除模糊的區域,為了區隔「什麼時候要擦」,可以計算「食指和中指的距離」,如果距離比較大就不擦 ( 手勢 YA ),距離比較小就擦 ( 兩隻手指併攏 )。
import cv2
import mediapipe as mp
import numpy as np
import math
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands
cap = cv2.VideoCapture(0)            # 讀取攝影機
# mediapipe 啟用偵測手掌
with mp_hands.Hands(
    model_complexity=0,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5) as hands:
    if not cap.isOpened():
        print("Cannot open camera")
        exit()
    w = 640    # 定義影片寬度
    h = 360    # 定義影像高度
    dots = []  # 記錄座標
    mask_b = np.zeros((h,w,3), dtype='uint8')            # 產生黑色遮罩 -> 套用清楚影像
    mask_w = np.zeros((h,w,3), dtype='uint8')            # 產生白色遮罩 -> 套用模糊影像
    mask_w[0:h, 0:w] = 255                               # 白色遮罩背景為白色
    while True:
        ret, img = cap.read()
        img = cv2.resize(img, (w,h))                     # 縮小尺寸,加快處理效率
        img = cv2.flip(img, 1)                           # 翻轉影像
        img_hand = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 偵測手勢使用
        img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)      # 轉換顏色為 BGRA ( 計算時需要用到 Alpha 色版 )
        img2 = img.copy()                                # 複製影像
        img2 = cv2.blur(img, (55, 55))                   # 套用模糊
        if not ret:
            print("Cannot receive frame")
            break
        results = hands.process(img_hand)                # 偵測手勢
        if results.multi_hand_landmarks:
            for hand_landmarks in results.multi_hand_landmarks:
                finger_points = []                       # 記錄手指節點位置的串列
                for i in hand_landmarks.landmark:
                    x = i.x
                    y = i.y
                    finger_points.append((x,y))          # 記錄手指節點位置
                if finger_points:
                    fx1 = finger_points[8][0]
                    fy1 = finger_points[8][1]
                    fx2 = finger_points[12][0]
                    fy2 = finger_points[12][1]
                    d = ((fx1-fx2)*(fx1-fx2)+(fy1-fy2)*(fy1-fy2))**0.5  # 計算食指和中指分開的距離
                    if d<0.15:
                        dots.append([fx1,fy1])
                        dl = len(dots)
                        if dl>1:
                            x1 = int(dots[dl-2][0]*w)   # 計算出真正的座標
                            y1 = int(dots[dl-2][1]*h)
                            x2 = int(dots[dl-1][0]*w)
                            y2 = int(dots[dl-1][1]*h)
                            cv2.line(mask_w, (x1,y1), (x2,y2), (0,0,0), 50)        # 在白色遮罩上畫出黑色線條
                            cv2.line(mask_b, (x1,y1), (x2,y2), (255,255,255), 50)  # 在黑色遮罩上畫出白色線條
                    else:
                        dots = []
        mask1 = cv2.cvtColor(mask_b, cv2.COLOR_BGR2GRAY) # 轉換遮罩為灰階
        img = cv2.bitwise_and(img, img, mask=mask1)      # 清楚影像套用黑遮罩
        mask2 = cv2.cvtColor(mask_w, cv2.COLOR_BGR2GRAY) # 轉換遮罩為灰階
        img2 = cv2.bitwise_and(img2, img2, mask=mask2)   # 模糊影像套用白遮罩
        output = cv2.add(img, img2)                      # 合併影像
        cv2.imshow('oxxostudio', output)
        keyboard = cv2.waitKey(5)
        if keyboard == ord('q'):
            break
cap.release()
cv2.destroyAllWindows()

大家好,我是 OXXO,是個即將邁入中年的斜槓青年,我已經寫了超過 400 篇 Python 的教學,有興趣可以參考下方連結呦~ ^_^